package org.planx.xmlstore.routing;

import java.io.*;
import java.math.*;
import java.net.*;
import java.util.*;
import junit.framework.*;
import org.planx.xmlstore.routing.messaging.*;
import org.planx.xmlstore.routing.operation.*;

public class KademliaTest extends RoutingTestCase {

    public KademliaTest(String s) {
        super(s);
    }

    public static Test suite() {
        return new TestSuite(KademliaTest.class);
    }

    public static void main(String args[]) {
        junit.textui.TestRunner.run(KademliaTest.class);
    }

    /**
     * Tests a simple "connect" of two peers.
     **/
    public void testConnect1() throws Exception {
        Kademlia kad1 = new Kademlia("PerfTest-1",Identifier.randomIdentifier(), 13333,null,null,"");
        Kademlia kad2 = new Kademlia("PerfTest-2",Identifier.randomIdentifier(), 14444,null,null,"");
        kad2.connect(new InetSocketAddress(InetAddress.getLocalHost(), 13333));
        List l1 = kad1.internalGetSpace().getAll();
        List l2 = kad2.internalGetSpace().getAll();
        orderNodes(l1);
        orderNodes(l2);
        kad1.close();
        kad2.close();
        assertEquals(l1, l2);
    }

    /**
     * Tests timeout when the bootstrap node does not respond.
     **/
    public void testConnect2() throws Exception {
        Configuration conf = new Configuration();
        conf.RESPONSE_TIMEOUT = 100;
        Kademlia kad1 = new Kademlia("Test-perf3",Identifier.randomIdentifier(), 13333, null,conf,"");
        try {
            kad1.connect(new InetSocketAddress(InetAddress.getLocalHost(), 4000));
            fail("Should have thrown KademliaException");
        } catch (RoutingException e) {
            assertTrue(true);
        } finally {
            kad1.close();
        }
    }

    /**
     * Tests put and get with only one node.
     **/
    public void testAtomNetwork() throws Exception {
        Kademlia kad1 = new Kademlia("Test-Perf4",Identifier.randomIdentifier(), 4000,null,null,"");
        Identifier id = Identifier.randomIdentifier();
        kad1.put(id, "Hvad er klokken?");
        String result = (String) kad1.get(id);
        kad1.close();
        assertEquals("Hvad er klokken?", result);
    }

    /**
     * Tests put and get with two nodes and lookup where node has failed.
     **/
    public void testSimpleNetwork() throws Exception {
        Kademlia kad1 = new Kademlia("test-perf5",Identifier.randomIdentifier(), 4000,null,null,"");
        Kademlia kad2 = new Kademlia("test-perf6",Identifier.randomIdentifier(), 5000,null,null,"");
        kad2.connect(new InetSocketAddress(InetAddress.getLocalHost(), 4000));

        Identifier id = Identifier.randomIdentifier();
        kad1.put(id, "Hvad er klokken?");
        String result = (String) kad2.get(id);
        assertEquals("Hvad er klokken?", result);

        kad1.close();

        result = (String) kad2.get(Identifier.randomIdentifier());
        assertEquals(null, result);
        kad2.close();
    }

    /**
     * Creates 8 nodes, bootstraps node 1-7 from node 0, stores a mapping from
     * node 1 and looks it up from node 2. K is 2 and the mapping is actually
     * stored at nodes 6 and 7 (not verified).
     **/
    public void testBasicNetwork() throws Exception {
        Identifier.IDSIZE = 8;
        Configuration conf = new Configuration();
        conf.K = 2;
        conf.B = 1;
        Kademlia[] kad = new Kademlia[8];

        // Create kademlia instances on different UDP ports and connect them
        for (int i = 0; i < 8; i++) {
            kad[i] = new Kademlia("Test-perf10"+i,newAbsId(i*32), 3000+i*32,null, conf,"");
            if (i>0) kad[i].connect(new InetSocketAddress(InetAddress.getLocalHost(), 3000));
        }

        // Test
        kad[1].put(newAbsId(220), "Hvad er klokken?");
        String result = (String) kad[2].get(newAbsId(220));

        // Close all
        for (int i = 0; i < 8; i++) kad[i].close();

        assertEquals("Hvad er klokken?", result);
    }

    /**
     * Tests that mappings are moved to a newly joined node.
     **/
    public void testMoveAndRemove() throws Exception {
        Identifier.IDSIZE = 8;
        Configuration conf = new Configuration();
        conf.K = 3;
        conf.B = 1;
        Kademlia[] kad = new Kademlia[8];

        // Create nodes 0 to 3
        for (int i = 0; i < 4; i++) {
            kad[i] = new Kademlia("Test-perf20"+i,newAbsId(i*32), 3000+i*32, null,conf,"");
            if (i>0) kad[i].connect(new InetSocketAddress(InetAddress.getLocalHost(), 3000));
        }

        kad[1].put(newAbsId(220), "Hvad er klokken?");

        for (int i = 4; i < 8; i++) {
            kad[i] = new Kademlia("Test-perf20"+i,newAbsId(i*32), 3000+i*32, null,conf,"");
            kad[i].connect(new InetSocketAddress(InetAddress.getLocalHost(), 3000));
        }

        for (int i = 0; i < 4; i++) kad[i].close();

        String result = (String) kad[4].get(newAbsId(220));
        assertEquals("Hvad er klokken?", result);

        kad[4].remove(newAbsId(220));

        synchronized (this) {wait(1000);}

        result = (String) kad[7].get(newAbsId(220));
        assertEquals(null, result);

        for (int i = 4; i < 8; i++) kad[i].close();
    }

    /**
     * Tests that a mapping is moved after a restore.
     * 1) A mapping is stored at K nodes.
     * 2) K-1 of these nodes fail.
     * 3) A restore operation is performed.
     * 4) The Kth node fail.
     * 5) The mapping is lookup and should still be available.
     **/
    public void testRestore() throws Exception {
        Identifier.IDSIZE = 8;
        Configuration conf = new Configuration();
        conf.K = 3;
        conf.B = 1;
        conf.STALE = 0;
        conf.RESTORE_INTERVAL = 3*1000;
        conf.RESPONSE_TIMEOUT = 100;
        Kademlia[] kad = new Kademlia[8];

        // Create nodes 0 to 7
        for (int i = 0; i < 8; i++) {
            kad[i] = new Kademlia("Test-perf30"+i,newAbsId(i*32), 3000+i*32, null,conf,"");
            if (i>0) kad[i].connect(new InetSocketAddress(InetAddress.getLocalHost(),
                                                          3000+(i-1)*32));
        }

        // Mapping stored at 0,1,2
        kad[0].put(newAbsId(10), "Hvad er klokken nu?");

        // Close node 0 and 1, mapping remains at 2
        kad[0].close();
        kad[1].close();

        // Wait for restore to take place, mapping should be copied to node 3,4
        synchronized (this) {
            wait(5*1000);
        }

        // Close node 2 - last of the original nodes containing the mapping
        kad[2].close();

        // Lookup mapping, should be available at node 3,4
        String result = (String) kad[4].get(newAbsId(10));

        // Close rest
        kad[3].close();
        kad[4].close();
        kad[5].close();
        kad[6].close();
        kad[7].close();

        assertEquals("Hvad er klokken nu?", result);
    }

    /**
     * Tests that mappings are removed after restore.
     **/
    public void testExpiration() throws Exception {
        Identifier.IDSIZE = 8;
        Configuration conf = new Configuration();
        conf.K = 1;
        conf.STALE = 0;
        conf.RESTORE_INTERVAL = 500;
        conf.RESPONSE_TIMEOUT = 100;

        Kademlia kad1 = new Kademlia("Test-perf401",newAbsId(0), 4000, null,conf,"");
        Kademlia kad2 = new Kademlia("Test-perf402",newAbsId(128), 5000,null, conf,"");
        kad1.put(newAbsId(0), "Peter Plys");
        kad1.put(newAbsId(128), "Honning");
        kad2.connect(new InetSocketAddress(InetAddress.getLocalHost(), 4000));

        // Wait for restore to take place, "Honning" should be removed from 1
        synchronized (this) {
            wait(800);
        }
        kad2.close();

        String result1 = (String) kad1.get(newAbsId(0));
        String result2 = (String) kad1.get(newAbsId(128));

        kad1.close();

        assertEquals("Peter Plys", result1);
        assertEquals(null, result2);
    }
}
